# ============================================================================
# 全局导入
# ============================================================================
import os
import cv2
import subprocess
import os.path as osp
import numpy as np
import time
import json
import acl  # 华为昇腾 pyacl 库
import paramiko  # 用于SFTP上传

# ============================================================================
# 关键修改：定义SDK路径并添加到环境变量中
# ============================================================================
SDK_LIB_PATH = "/home/HwHiAiUser/table_tennis/egs_nvr-master/lib"

if SDK_LIB_PATH not in os.environ.get('LD_LIBRARY_PATH', ''):
    print(f"将SDK路径 {SDK_LIB_PATH} 添加到环境变量中...")
    os.environ['LD_LIBRARY_PATH'] = f"{SDK_LIB_PATH}:{os.environ.get('LD_LIBRARY_PATH', '')}"

net_sdk_dll_path = os.path.join(SDK_LIB_PATH, "libNetDEVSDK.so")

try:
    from netdevsdk import *
except ImportError:
    print("警告：无法从'netdevsdk'导入类型定义，将使用占位符。")

    class c_void_p: pass
    class POINTER: pass
    class c_int: pass
    class c_char_p: pass
    class c_int64: pass
    class c_int32: pass
    class byref: pass
    class pointer: pass
    def load_library(path):
        from ctypes import cdll
        return cdll.LoadLibrary(path)
    class NETDEV_DEVICE_LOGIN_INFO_S: pass
    class NETDEV_SELOG_INFO_S: pass
    class NETDEV_PLAYBACKCOND_S: pass
    class NETDEV_VIDEO_CHL_DETAIL_INFO_S: pass


# ============================================================================
# 模块 1: 视频拉取 (来自 File 1.py)
# ============================================================================
class DevClass:
    """
    与NVR/IPC设备交互以下载视频的类。
    """
    def __init__(self):
        self.egsSdk = self.load_sdk()
        self.iUserID = -1

    def load_sdk(self):
        egs_sdk = None
        try:
            print(f"正在从路径加载动态库: {net_sdk_dll_path}")
            egs_sdk = load_library(net_sdk_dll_path)
            print("动态库加载成功。")

            # NETDEV_Login_V30
            egs_sdk.NETDEV_Login_V30.restype = c_void_p
            egs_sdk.NETDEV_Login_V30.argtypes = [POINTER(NETDEV_DEVICE_LOGIN_INFO_S), POINTER(NETDEV_SELOG_INFO_S)]

            # NETDEV_QueryVideoChlDetailList
            egs_sdk.NETDEV_QueryVideoChlDetailList.restype = c_int
            egs_sdk.NETDEV_QueryVideoChlDetailList.argtypes = [c_void_p, POINTER(c_int32), POINTER(NETDEV_VIDEO_CHL_DETAIL_INFO_S)]

            # NETDEV_GetFileByTime
            egs_sdk.NETDEV_GetFileByTime.restype = c_void_p
            egs_sdk.NETDEV_GetFileByTime.argtypes = [c_void_p, POINTER(NETDEV_PLAYBACKCOND_S), c_char_p, c_int]

            # NETDEV_Logout
            egs_sdk.NETDEV_Logout.restype = c_int
            egs_sdk.NETDEV_Logout.argtypes = [c_void_p]

            # NETDEV_PlayBackControl
            egs_sdk.NETDEV_PlayBackControl.restype = c_int
            egs_sdk.NETDEV_PlayBackControl.argtypes = [c_void_p, c_int, c_void_p]

        except Exception as e:
            print(f"动态库加载失败，请检查路径 '{net_sdk_dll_path}' 是否正确以及文件权限: {e}")
        return egs_sdk


    def sdk_init(self):
        if not self.egsSdk:
            print("错误：SDK库未成功加载，无法初始化。")
            return False

        # 这里调用初始化
        ret = self.egsSdk.NETDEV_Init()
        if ret != 0:
            print(f"初始化SDK失败，返回码: {ret}。请检查LD_LIBRARY_PATH是否包含所有必需的.so文件。")
            return False

        print("SDK 初始化成功。")
        return True


    def login(self, ip, port, username, password):
        if not self.egsSdk:
            return False

        login_info = NETDEV_DEVICE_LOGIN_INFO_S()
        login_info.szIPAddr = ip
        login_info.dwPort = port
        login_info.szUserName = username
        login_info.szPassword = password
        login_info.dwLoginProto = 0  # TCP
        login_info.dwDeviceType = 0

        se_log_info = NETDEV_SELOG_INFO_S()
        self.iUserID = self.egsSdk.NETDEV_Login_V30(pointer(login_info), pointer(se_log_info))

    if self.iUserID == 0:
        print(f"登陆失败，错误码：{self.egsSdk.NETDEV_GetLastError()}")
        return False
    print("设备登录成功。")
    return True



    def download(self, channel_id, begin_time, end_time, save_filename):
        if not self.egsSdk or self.iUserID <= 0: return False

        # 确保保存视频的目录存在
        os.makedirs(os.path.dirname(save_filename), exist_ok=True)
        
        # ============================================================================
        # =====> 核心修改: 增加了这个函数调用，以匹配 "正确版本" 的行为 <=====
        # ============================================================================
        try:
            print("正在查询通道详情列表 (这可能是下载前的必需步骤)...")
            dInfo = NETDEV_VIDEO_CHL_DETAIL_INFO_S()
            channel_count = c_int32(128) # 分配足够大的空间
            bRet = self.egsSdk.NETDEV_QueryVideoChlDetailList(self.iUserID, byref(channel_count), byref(dInfo))
            if bRet == 0:
                 print(f"查询通道详情列表成功。通道数: {channel_count.value}")
            else:
                 # 即使失败也只打印警告，因为主要目的是下载
                 print(f"警告: 查询通道详情列表失败，错误码: {self.egsSdk.NETDEV_GetLastError()}")
        except Exception as e:
            print(f"警告: 调用 NETDEV_QueryVideoChlDetailList 时发生异常: {e}")
        # ============================================================================
        # =====> 修改结束 <=====
        # ============================================================================

        playback_cond = NETDEV_PLAYBACKCOND_S()
        playback_cond.dwChannelID = channel_id
        playback_cond.tBeginTime = begin_time
        playback_cond.tEndTime = end_time
        playback_cond.dwLinkMode = 1
        playback_cond.dwDownloadSpeed = 2

        get_file = self.egsSdk.NETDEV_GetFileByTime(self.iUserID, byref(playback_cond), save_filename.encode('utf-8'), 0)
        if get_file == 0:
            print(f"开始下载失败，错误码: {self.egsSdk.NETDEV_GetLastError()}")
            return False

        print("开始下载视频...")
        play_time = c_int64(0)
        time.sleep(2)

        while True:
            self.egsSdk.NETDEV_PlayBackControl(get_file, 3, byref(play_time))
            if play_time.value == 0:
                time.sleep(1)
                continue

            duration = end_time - begin_time
            if duration <= 0:
                print("错误：时间段无效。")
                # 需要停止下载句柄
                self.egsSdk.NETDEV_PlayBackControl(get_file, 1, None) # 1 通常是停止
                return False

            progress = ((play_time.value - begin_time) / duration) * 100
            progress = min(100.0, progress) # 防止超出100%
            print(f"\r当前下载进度：{progress:.2f}%", end="")

            if progress >= 100:
                print("\n视频下载完成。")
                break
            time.sleep(1)
        
        # 停止下载句柄
        self.egsSdk.NETDEV_PlayBackControl(get_file, 1, None)
        return True

    def logout(self):
        if self.egsSdk and self.iUserID > 0:
            if self.egsSdk.NETDEV_Logout(self.iUserID) == 0:
                print(f"登出失败，错误码：{self.egsSdk.NETDEV_GetLastError()}")
            else:
                print("设备登出成功。")

    def sdk_cleanup(self):
        if self.egsSdk:
            if self.egsSdk.NETDEV_Cleanup() != 0:
                print("释放SDK失败")

    def __del__(self):
        self.logout()
        self.sdk_cleanup()


# ============================================================================
# 模块 2: 视频处理 (来自 File 4.py) - 在RTSP版本中不再使用
# ============================================================================
def video_to_images(vid_file, img_folder=None, return_info=False):
    # ... 此函数保持不变，但在新的main函数中不会被调用 ...
    """使用 ffmpeg 将视频文件转换为一系列 PNG 图像。"""
    if not osp.exists(vid_file):
        print(f"错误：视频文件不存在于路径 {vid_file}")
        return None
    if img_folder is None:
        img_folder = osp.join('./temp_images', osp.basename(vid_file).replace('.', '_'))

    os.makedirs(img_folder, exist_ok=True)

    command = ['ffmpeg',
               '-i', vid_file,
               '-f', 'image2',
               '-v', 'error',
               f'{img_folder}/%06d.png']
    print(f'正在执行命令： "{" ".join(command)}"')
    subprocess.call(command)

    print(f'图像已保存至： "{img_folder}"')

    first_image_path = osp.join(img_folder, '000001.png')
    if not os.path.exists(first_image_path):
        print("警告: 未找到第一张图像。视频可能为空或无效。")
        if return_info:
            return None, 0, (0, 0, 0)
        else:
            return None

    img_shape = cv2.imread(first_image_path).shape
    num_images = len(os.listdir(img_folder))

    if return_info:
        return img_folder, num_images, img_shape
    else:
        return img_folder



# ============================================================================
# 模块 3: AI 推理 (来自 File 3.py) - 保持不变
# ============================================================================

# 3.1 ACL 常量
ACL_MEM_MALLOC_HUGE_FIRST = 0
ACL_MEMCPY_HOST_TO_DEVICE = 1
ACL_MEMCPY_DEVICE_TO_HOST = 2
ACL_SUCCESS = 0

# 3.2 图像预处理和后处理函数
def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True, stride=32):
    shape = im.shape[:2]
    if isinstance(new_shape, int): new_shape = (new_shape, new_shape)
    r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
    if not scaleup: r = min(r, 1.0)
    ratio = r, r
    new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
    dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]
    if auto: dw, dh = np.mod(dw, stride), np.mod(dh, stride)
    dw /= 2; dh /= 2
    if shape[::-1] != new_unpad: im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR)
    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
    im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
    return im, ratio, (dw, dh)

def preprocess(image_path, model_input_size=(640, 640)):
    print(f"预处理图像: {image_path}，目标尺寸 {model_input_size}")
    img0 = cv2.imread(image_path)
    if img0 is None: raise FileNotFoundError(f"无法读取图像文件: {image_path}")
    img, _, _ = letterbox(img0, new_shape=model_input_size, stride=32, auto=False)
    img = img.transpose((2, 0, 1))[::-1]
    img = np.ascontiguousarray(img, dtype=np.float32) / 255.0
    img = np.expand_dims(img, axis=0)
    print("预处理完成。")
    return img

def postprocess(preds, conf_thres=0.25, iou_thres=0.45):
    preds = preds.transpose(0, 2, 1)
    boxes, scores, class_ids = [], [], []
    for det in preds[0]:
        class_score = det[4:]
        max_score_index, max_score = np.argmax(class_score), np.max(class_score)
        if max_score > conf_thres:
            scores.append(float(max_score))
            class_ids.append(max_score_index)
            cx, cy, w, h = det[:4]
            x, y = int(cx - w / 2), int(cy - h / 2)
            boxes.append([x, y, int(w), int(h)])
    indices = cv2.dnn.NMSBoxes(boxes, scores, score_threshold=conf_thres, nms_threshold=iou_thres)
    final_class_ids = []
    if len(indices) > 0:
        final_class_ids = [class_ids[i] for i in indices.flatten()]
    return final_class_ids

# 3.3 ACL 推理器封装 (完整版)
class YOLOv8_ACL_Detector:
    def __init__(self, model_path):
        self.model_path = model_path
        self.model_id = None
        self.model_desc = None
        self.context = None
        self.stream = None
        self.input_dataset, self.output_dataset = None, None
        self.input_buffer, self.output_buffers = None, []
        self.input_buffer_size, self.output_sizes = 0, []

    def init(self):
        print("Initializing detector resources (Context, Stream, Model)...")
        self.context, ret = acl.rt.create_context(0)
        if ret != ACL_SUCCESS: raise RuntimeError(f"create_context failed, ret={ret}")
        self.stream, ret = acl.rt.create_stream()
        if ret != ACL_SUCCESS: raise RuntimeError(f"create_stream failed, ret={ret}")
        print(f"Loading model from: {self.model_path}")
        self.model_id, ret = acl.mdl.load_from_file(self.model_path)
        if ret != ACL_SUCCESS: raise RuntimeError(f"load_from_file failed, ret={ret}")
        self.model_desc = acl.mdl.create_desc()
        ret = acl.mdl.get_desc(self.model_desc, self.model_id)
        if ret != ACL_SUCCESS: raise RuntimeError(f"get_desc failed, ret={ret}")
        self._init_buffers()
        print("Detector resources initialized successfully.")

    def _init_buffers(self):
        self.input_dataset = acl.mdl.create_dataset()
        self.input_buffer_size = acl.mdl.get_input_size_by_index(self.model_desc, 0)
        self.input_buffer, ret = acl.rt.malloc(self.input_buffer_size, ACL_MEM_MALLOC_HUGE_FIRST)
        if ret != ACL_SUCCESS: raise RuntimeError(f"malloc for input failed, ret={ret}")
        data_buf = acl.create_data_buffer(self.input_buffer, self.input_buffer_size)
        acl.mdl.add_dataset_buffer(self.input_dataset, data_buf)
        self.output_dataset = acl.mdl.create_dataset()
        num_outputs = acl.mdl.get_num_outputs(self.model_desc)
        for i in range(num_outputs):
            size = acl.mdl.get_output_size_by_index(self.model_desc, i)
            self.output_sizes.append(size)
            buffer, ret = acl.rt.malloc(size, ACL_MEM_MALLOC_HUGE_FIRST)
            if ret != ACL_SUCCESS: raise RuntimeError(f"malloc for output {i} failed, ret={ret}")
            self.output_buffers.append(buffer)
            data_buf = acl.create_data_buffer(buffer, size)
            acl.mdl.add_dataset_buffer(self.output_dataset, data_buf)

    def run(self, input_data):
        print(len(input_data.tobytes()))
        ptr = acl.util.bytes_to_ptr(input_data.tobytes())
        ret = acl.rt.memcpy(self.input_buffer, self.input_buffer_size, ptr, self.input_buffer_size, ACL_MEMCPY_HOST_TO_DEVICE)
        if ret != ACL_SUCCESS: raise RuntimeError(f"memcpy H2D failed, ret={ret}")
        ret = acl.mdl.execute(self.model_id, self.input_dataset, self.output_dataset)
        if ret != ACL_SUCCESS: raise RuntimeError(f"execute failed, ret={ret}")
        outputs = []
        for i, size in enumerate(self.output_sizes):
            host_buffer, ret = acl.rt.malloc_host(size)
            if ret != ACL_SUCCESS: raise RuntimeError(f"malloc_host failed, ret={ret}")
            try:
                ret = acl.rt.memcpy(host_buffer, size, self.output_buffers[i], size, ACL_MEMCPY_DEVICE_TO_HOST)
                if ret != ACL_SUCCESS: raise RuntimeError(f"memcpy D2H failed, ret={ret}")
                output_bytes = acl.util.ptr_to_bytes(host_buffer, size)
                dims_info, _ = acl.mdl.get_cur_output_dims(self.model_desc, i)
                shape = tuple(dims_info['dims'])
                np_array = np.frombuffer(output_bytes, dtype=np.float32).reshape(shape)
                outputs.append(np_array)
            finally:
                acl.rt.free_host(host_buffer)
        return outputs

    def release(self):
        print("Releasing detector resources...")
        if self.model_id: acl.mdl.unload(self.model_id)
        if self.model_desc: acl.mdl.destroy_desc(self.model_desc)
        if self.input_buffer: acl.rt.free(self.input_buffer)
        if self.input_dataset: acl.mdl.destroy_dataset(self.input_dataset)
        for buffer in self.output_buffers:
            if buffer: acl.rt.free(buffer)
        if self.output_dataset: acl.mdl.destroy_dataset(self.output_dataset)
        if self.stream: acl.rt.destroy_stream(self.stream)
        if self.context: acl.rt.destroy_context(self.context)
        print("Detector resources released.")


# ============================================================================
# 模块 4: SFTP 上传功能 - 保持不变
# ============================================================================
def upload_file_sftp(local_path, remote_path, hostname, username, password, port=22):
    """
    使用SFTP将本地文件上传到远程服务器。
    """
    transport = None
    sftp = None
    try:
        print(f"正在连接到 SFTP 服务器 {hostname}...")
        transport = paramiko.Transport((hostname, port))
        transport.connect(username=username, password=password)
        sftp = paramiko.SFTPClient.from_transport(transport)

        remote_dir = os.path.dirname(remote_path)
        if remote_dir:
            try:
                sftp.stat(remote_dir)
            except FileNotFoundError:
                print(f"远程目录 {remote_dir} 不存在，正在尝试创建...")
                # 简单的递归创建目录
                current_dir = ''
                for dir_part in remote_dir.split('/'):
                    if not dir_part: continue
                    current_dir += '/' + dir_part
                    try:
                        sftp.stat(current_dir)
                    except FileNotFoundError:
                        sftp.mkdir(current_dir)

        print(f"正在上传本地文件 {local_path} 到远程路径 {remote_path}...")
        sftp.put(local_path, remote_path)
        print("文件上传成功。")

    except Exception as e:
        print(f"SFTP 上传过程中发生错误: {e}")
    finally:
        if sftp:
            sftp.close()
        if transport:
            transport.close()
        print("SFTP 连接已关闭。")

# ============================================================================
# 主逻辑 / Main (扩展版)
# ============================================================================
def main():
    # --- 1. 配置参数 ---
    # NVR/摄像头参数
    NVR_IP = b"192.168.1.30"
    NVR_PORT = 80
    NVR_USERNAME = b"admin"
    NVR_PASSWORD = b"Nanjing@8625"
    NVR_CHANNEL_ID = 1
    CAMERA_CODE = "210235C71P3254000069"  # 摄像头CODE，用于构建服务器路径

    # <-- !!重要!! 请在此处填写您的SFTP服务器信息 -->
    SFTP_HOSTNAME = "192.168.1.107"
    SFTP_USERNAME = "ywt"
    SFTP_PASSWORD = "passwd"
    SFTP_PORT = 22
    SFTP_BASE_REMOTE_PATH = "/data/minio/data/zlmediakit/live"

    # 视频下载时间（使用UNIX时间戳）
    # 注意：为了生成可预测的文件名和路径，我们从时间戳中提取日期
    import datetime
    end_time = 1748931713
    begin_time = 1748931710  # 下载8秒的视频作为示例
    video_start_dt = datetime.datetime.fromtimestamp(begin_time)
    date_path = video_start_dt.strftime('%Y/%m/%d')  # 生成 "2025/05/14" 格式的路径
    video_duration = end_time - begin_time

    # 文件和路径参数
    LOCAL_BASE_DIR = os.path.expanduser("~/table_tennis/egs_nvr-master") # 主工作目录
    LOCAL_VIDEO_DIR = os.path.join(LOCAL_BASE_DIR, "downloaded_videos")
    LOCAL_IMAGE_DIR = os.path.join(LOCAL_BASE_DIR, "temp_images")
    OUTPUT_DIR = os.path.join(LOCAL_BASE_DIR, "output")  # VIBE及后续处理的根输出目录
    VIDEO_FILENAME = os.path.join(LOCAL_VIDEO_DIR, f"{begin_time}_{video_duration}.mp4")

    # AI模型参数 (YOLOv8)
    MODEL_PATH = "/home/HwHiAiUser/table_tennis/modelnew/yolov8n_bs1.om"
    MODEL_INPUT_SIZE = (640, 640)

    # --- 2. 初始化对象 ---
    dev = DevClass()
    detector = None

    try:
        # ====================================================================
        # 阶段 1: NVR视频下载与YOLO人数检测 (此部分逻辑来自您的原始代码)
        # ====================================================================
        print("--- 阶段 1: 下载视频并检测人数 ---")
        if not dev.sdk_init():
             raise SystemError("SDK 初始化失败。")
        if not dev.login(NVR_IP, NVR_PORT, NVR_USERNAME, NVR_PASSWORD):
            raise ConnectionError("NVR 登录失败。")

        os.makedirs(LOCAL_VIDEO_DIR, exist_ok=True)
        if not dev.download(NVR_CHANNEL_ID, begin_time, end_time, VIDEO_FILENAME):
            raise IOError("视频下载失败。")

        image_folder = video_to_images(VIDEO_FILENAME, LOCAL_IMAGE_DIR)
        if not image_folder:
            raise RuntimeError("视频切片失败，无法继续。")

        image_files = sorted([f for f in os.listdir(image_folder) if f.lower().endswith('.png')])
        if not image_files:
            raise RuntimeError("图像文件夹为空，无法进行推理。")

        target_image_path = os.path.join(image_folder, image_files[len(image_files) // 2])

        # ACL 初始化和YOLO推理
        print("正在初始化ACL进程...")
        ret = acl.init()
        if ret != ACL_SUCCESS: raise RuntimeError(f"acl.init 失败, ret={ret}")
        ret = acl.rt.set_device(0)
        if ret != ACL_SUCCESS: raise RuntimeError(f"set_device 失败, ret={ret}")
        
        input_data = preprocess(target_image_path, model_input_size=MODEL_INPUT_SIZE)
        detector = YOLOv8_ACL_Detector(MODEL_PATH)
        detector.init()

        if input_data.nbytes != detector.input_buffer_size:
            raise ValueError(f"预处理后的数据大小({input_data.nbytes})与模型输入大小({detector.input_buffer_size})不匹配!")

        outputs = detector.run(input_data)
        detected_cls_ids = postprocess(outputs[0])
        person_count = sum(1 for cls_id in detected_cls_ids if int(cls_id) == 0)
        print(f"检测完成。图像中的人数为: {person_count}")

        # 生成人数JSON (result_place.json)
        person_num_data = {"placedata": {"HumanNumber": person_count}}
        LOCAL_PERSON_NUM_JSON_PATH = os.path.join(OUTPUT_DIR, "personNum", "result_place.json")
        os.makedirs(os.path.dirname(LOCAL_PERSON_NUM_JSON_PATH), exist_ok=True)
        with open(LOCAL_PERSON_NUM_JSON_PATH, 'w') as f:
            json.dump(person_num_data, f, indent=2)
        print(f"人数结果已保存到本地: {LOCAL_PERSON_NUM_JSON_PATH}")

        # ====================================================================
        # 阶段 2: VIBE处理、数据转换和最终JSON整合 (新增逻辑)
        # ====================================================================
        print("\n--- 阶段 2: 开始VIBE三维姿态估计和数据转换 ---")
        
        # 路径准备
        video_basename_for_vibe = os.path.splitext(os.path.basename(VIDEO_FILENAME))[0]
        VIBE_OUTPUT_DIR = os.path.join(OUTPUT_DIR, video_basename_for_vibe)
        VIBE_PKL_PATH = os.path.join(VIBE_OUTPUT_DIR, "vibe_output.pkl")

        # 1. 运行VIBE输出pkl
        print("--> 步骤 2.1: 运行VIBE生成.pkl文件...")
        subprocess.run(['python', 'demo.py', '--vid_file', VIDEO_FILENAME, '--output_folder', OUTPUT_DIR], check=True)
        if not os.path.exists(VIBE_PKL_PATH):
            raise FileNotFoundError(f"VIBE输出文件 {VIBE_PKL_PATH} 未找到，处理中断。")

        # 2. 将pkl转化为glb (为左右两个人分别生成)
        print("--> 步骤 2.2: 将.pkl转换为.glb动画文件...")
        os.makedirs(os.path.join(VIBE_OUTPUT_DIR, "pose"), exist_ok=True) # 确保pose子目录存在
        for person_id in range(1, min(person_count, 2) + 1):  # 最多处理两个人
            side = 'left' if person_id == 1 else 'right'
            # 目标文件名格式： sample_left_000000_60.glb
            output_glb_path = os.path.join(VIBE_OUTPUT_DIR, "pose", f"sample_{side}_{begin_time}_{video_duration}.glb")
            subprocess.run([
                'python', 'lib/utils/create_animated_glb.py',
                '--input_motion', VIBE_PKL_PATH,
                '--input_model', 'dataset/test/models/smplx/SMPLX_FEMALE.npz',
                '--output', output_glb_path,
                '--gender', 'female',
                '--fps_target', '30',
                '--person_id', str(person_id)
            ], check=True)
            print(f"已为 Person {person_id} ({side}) 生成GLB文件: {output_glb_path}")
        
        # 3. 后续所有JSON转换步骤
        print("--> 步骤 2.3 - 2.8: 开始执行系列JSON转换...")
        raw_json_path = os.path.join(VIBE_OUTPUT_DIR, "raw_skeleton.json")
        subprocess.run(['python', 'lib/utils/pkl_to_json.py', '--input', VIBE_PKL_PATH, '--output', raw_json_path], check=True)

        target_skeleton_json_path = os.path.join(VIBE_OUTPUT_DIR, "output_skeleton.json")
        subprocess.run(['python', 'lib/utils/both_json.py', '--input', raw_json_path, '--output', target_skeleton_json_path], check=True)

        ball_data_json_path = os.path.join(OUTPUT_DIR, "ball/balldata.json")
        os.makedirs(os.path.dirname(ball_data_json_path), exist_ok=True)
        subprocess.run(['python', 'main.py', '--input', VIDEO_FILENAME, '--output', ball_data_json_path], check=True)

        athlete_json_path = os.path.join(VIBE_OUTPUT_DIR, "output_athlete.json")
        subprocess.run(['python', 'lib/utils/both_json2.py', '--input', target_skeleton_json_path, '--ball', ball_data_json_path, '--output', athlete_json_path], check=True)
        
        # 为左右运动员分别生成 keypoints.json
        # 假设 both_json.py 已经将两人数据分离，或我们需要自己处理
        # 这里我们先用一个文件代替，实际可能需要修改 both_json.py 或添加新脚本
        keypoints_left_path = os.path.join(VIBE_OUTPUT_DIR, "pose", f"keypoints_left_{begin_time}_{video_duration}.json")
        keypoints_right_path = os.path.join(VIBE_OUTPUT_DIR, "pose", f"keypoints_right_{begin_time}_{video_duration}.json")
        import shutil
        shutil.copy(target_skeleton_json_path, keypoints_left_path) # 简化处理：复制同一个文件
        if person_count > 1:
            shutil.copy(target_skeleton_json_path, keypoints_right_path)

        # 整合最终的 result.json 用于 wonderful 目录
        final_wonderful_json_path = os.path.join(VIBE_OUTPUT_DIR, f"{begin_time}_{video_duration}.json")
        subprocess.run(['python', 'lib/utils/result.py', '--skeleton', target_skeleton_json_path, '--ball', ball_data_json_path, '--athlete', athlete_json_path, '--output', final_wonderful_json_path], check=True)

        # 创建 pose/duration.json
        duration_data = {"start_time": f"{begin_time}", "end_time": f"{end_time}"}
        duration_json_path = os.path.join(VIBE_OUTPUT_DIR, "pose", "duration.json")
        with open(duration_json_path, 'w') as f:
            json.dump(duration_data, f, indent=2)
            
        print(f"所有本地文件处理和转换完成。输出文件位于: {VIBE_OUTPUT_DIR}")

        # ====================================================================
        # 阶段 3: 组织文件并上传到SFTP服务器 (新增逻辑)
        # ====================================================================
        # ====================================================================
        # 阶段 3: 组织文件并上传到SFTP服务器
        # ====================================================================
        print("\n--- 阶段 3: 上传文件到SFTP服务器 ---")
        
        # <-- 已修改: 所有远程路径都加上了SFTP根目录 -->
        # 1. 上传人数文件
        remote_person_num_path = f"{SFTP_BASE_REMOTE_PATH}/{CAMERA_CODE}/personNum/result_place.json"
        upload_file_sftp(LOCAL_PERSON_NUM_JSON_PATH, remote_person_num_path, SFTP_HOSTNAME, SFTP_USERNAME, SFTP_PASSWORD, SFTP_PORT)

        # 2. 上传source目录下的文件
        remote_source_pose_dir = f"{SFTP_BASE_REMOTE_PATH}/{CAMERA_CODE}/{date_path}/source/pose"
        for person_id in range(1, min(person_count, 2) + 1):
            side = 'left' if person_id == 1 else 'right'
            local_glb = os.path.join(VIBE_OUTPUT_DIR, "pose", f"sample_{side}_{begin_time}_{video_duration}.glb")
            remote_glb = f"{remote_source_pose_dir}/sample_{side}_{begin_time}_{video_duration}.glb"
            upload_file_sftp(local_glb, remote_glb, SFTP_HOSTNAME, SFTP_USERNAME, SFTP_PASSWORD, SFTP_PORT)

            local_keypoints = os.path.join(VIBE_OUTPUT_DIR, "pose", f"keypoints_{side}_{begin_time}_{video_duration}.json")
            remote_keypoints = f"{remote_source_pose_dir}/keypoints_{side}_{begin_time}_{video_duration}.json"
            upload_file_sftp(local_keypoints, remote_keypoints, SFTP_HOSTNAME, SFTP_USERNAME, SFTP_PASSWORD, SFTP_PORT)
        
        # 3. 上传wonderful目录下的文件
        remote_wonderful_dir = f"{SFTP_BASE_REMOTE_PATH}/{CAMERA_CODE}/{date_path}/wonderful"
        upload_file_sftp(final_wonderful_json_path, f"{remote_wonderful_dir}/{begin_time}_{video_duration}.json", SFTP_HOSTNAME, SFTP_USERNAME, SFTP_PASSWORD, SFTP_PORT)
        upload_file_sftp(duration_json_path, f"{remote_wonderful_dir}/pose/duration.json", SFTP_HOSTNAME, SFTP_USERNAME, SFTP_PASSWORD, SFTP_PORT)
        
        print("所有文件已成功上传。")

    except Exception as e:
        print(f"主流程执行过程中发生严重错误: {e}")
        import traceback
        traceback.print_exc()
    finally:
        # --- 4. 清理所有资源 ---
        print("\n开始清理所有已分配的资源...")
        if detector and detector.context:
            detector.release()

        # 检查ACL是否已初始化再执行finalize
        try:
            # 这是一个检查是否初始化的技巧，如果没有初始化会抛出异常
            acl.rt.get_run_mode()
            print("正在终止ACL进程...")
            acl.finalize()
            print("ACL进程已终止。")
        except:
            pass # ACL未初始化，无需终止

        # 登出设备并清理SDK
        dev.logout()
        # dev.sdk_cleanup() is called automatically by __del__

        print("所有流程执行完毕。")


if __name__ == '__main__':
    main()